using System;
using System.Collections.Specialized;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.UI;

namespace SCS.Web.UI.WebControls
{
	/// <summary>
	/// Provides a secure means for transfering data within a query string.
	/// </summary>
	public class QueryString : NameValueCollection
    {
        public enum DuplicateActionType { RemoveTarget = 0, RemoveSource, CombineValues }

        #region Fields
        private const string _timeStampKey = "__TimeStamp__";

        // The key used for generating the encrypted string
        private string _cryptoKey = "";

        // The Initialization Vector for the DES encryption routine
        private readonly byte[] IV = new byte[8] { 240, 3, 45, 29, 0, 76, 173, 59 };

        // set default expiration to 60 mins
        private int _expireMinutes = 60;
        private DateTime _expireTime = DateTime.Now.AddMinutes(60);
        
        #endregion

        public QueryString() : base() 
        { 
        }
        public QueryString(string encryptionKey)
        {
            _cryptoKey = encryptionKey;
        }
        public QueryString(string queryString, string encryptionKey) 
		{
            _cryptoKey = encryptionKey;

            Parse(queryString);

			// Compare the Expiration Time with the current Time to ensure
			// that the queryString has not expired.
			if (DateTime.Compare(ExpireTime, DateTime.Now) < 0) 
			{
				throw new ApplicationException("Query string has expired.");
			}
		}

		/// <summary>
		/// Encrypts a serialized query string 
		/// </summary>
		private string Encrypt(string serializedQueryString) 
		{
			byte[] buffer = Encoding.ASCII.GetBytes(serializedQueryString);
			TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();
			MD5CryptoServiceProvider MD5 = new MD5CryptoServiceProvider();
			des.Key = MD5.ComputeHash(ASCIIEncoding.ASCII.GetBytes(_cryptoKey));
			des.IV = IV;
			return Convert.ToBase64String(
				des.CreateEncryptor().TransformFinalBlock(
					buffer,
					0,
					buffer.Length
				)
			);
		}

		/// <summary>
		/// Decrypts a serialized query string
		/// </summary>
        private string Decrypt(string encryptedQueryString) 
		{
			try 
			{
				byte[] buffer = Convert.FromBase64String(encryptedQueryString);
				TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();
				MD5CryptoServiceProvider MD5 = new MD5CryptoServiceProvider();
				des.Key = MD5.ComputeHash(ASCIIEncoding.ASCII.GetBytes(_cryptoKey));
				des.IV = IV;
				return Encoding.ASCII.GetString(
					des.CreateDecryptor().TransformFinalBlock(
						buffer,
						0,
						buffer.Length
					)
				);
			} 
			catch (CryptographicException ex1) 
			{
				throw new ApplicationException("The query string provided has not been formatted properly.", ex1);
			}
			catch (FormatException ex2) 
			{
				throw new ApplicationException("The query string provided has not been formatted properly.", ex2);
			}
		}

		/// <summary>
		/// Deserializes a decrypted query string and stores it
		/// as name/value pairs.
		/// </summary>
		public void Parse(string queryString) 
		{
            queryString = HttpUtility.UrlDecode(queryString);

            if (queryString.StartsWith("?"))
                queryString = queryString.Substring(1);

            if (queryString.Length == 0)
                return;

            if (queryString.IndexOf(MainKey + "=", StringComparison.OrdinalIgnoreCase) != -1)
            {
                queryString = queryString.Replace(MainKey + "=", "");
                queryString = Decrypt(queryString);
            }

            string[] nameValuePairs = queryString.Split('&');
			for (int i=0; i<nameValuePairs.Length; i++) 
			{
				string[] nameValue = nameValuePairs[i].Split('=');
				if (nameValue.Length == 2) 
				{
					base.Add(nameValue[0], nameValue[1]);
				}
			}
			// Ensure that timeStampKey exists and update the expiration time.
			if (base[_timeStampKey] != null) 
				_expireTime = DateTime.Parse(base[_timeStampKey]);
		}

		/// <summary>
		/// Serializes the underlying NameValueCollection as a QueryString
		/// </summary>
		private string Build(bool includeTimestamp)
		{
			StringBuilder sb = new StringBuilder();
			foreach (string key in base.AllKeys) 
			{
				sb.Append(key);
				sb.Append('=');
				sb.Append(base[key]);
				sb.Append('&');
			}

            if (includeTimestamp)
            {
                // Append timestamp
                sb.Append(_timeStampKey);
                sb.Append('=');
                sb.Append(_expireTime);
            }

			return sb.ToString();
        }

        /// <summary>
        /// Returns the complete query string.
        /// </summary>
        public override string ToString()
        {
            return Build(false);
        }

        public QueryString Merge(DuplicateActionType duplicateAction, NameValueCollection source)
        {
            return Merge(source, duplicateAction);
        }
        public QueryString Merge(NameValueCollection source, DuplicateActionType duplicateAction)
        {
            if (duplicateAction == DuplicateActionType.RemoveSource)
            {
                // remove duplicates from source query string
                foreach (string key in base.AllKeys) 
			    {
				    if (source[key] != null)
                        source.Remove(key);
			    }

                // append the filtered set of source query stgring param.
                base.Add(source);
            }
            else if (duplicateAction == DuplicateActionType.RemoveTarget)
            {
                // remove duplicate from this class (target).
                foreach (string key in source.AllKeys)
                {
                    if (base[key] != null)
                        base.Remove(key);
                }
                // append all the source query string params.
                base.Add(source);
            }
            else
            {
                // combine target values where same exists in source.
                foreach (string key in base.AllKeys)
                {
                    if (source[key] != null)
                        base[key] += "," + source[key];
                }

                // add souce query string params to target if they don't exisit.
                foreach (string key in source)
                {
                    if (base[key] == null)
                        base.Add(key, source[key]);
                }
            }

            return this;
        }

        /// <summary>
        /// Returns the complete query string.
        /// </summary>
        /// <param name="encryptQueryString">Whether to encrypt the query string.</param>
        /// <returns>Query string</returns>
        public string ToString(bool encryptQueryString)
        {
            if (encryptQueryString)
                return string.Format("{0}={1}", MainKey, HttpUtility.UrlEncode(Encrypt(Build(true))));
            else
                return ToString();            
        }
        public string ToString(bool encryptQueryString, bool includeQuestionMark)
        {
            if (includeQuestionMark)
                return "?" + ToString(encryptQueryString);
            else
                return ToString(encryptQueryString);
        }

        #region Properties

        /// <summary>
        /// The timestamp in which the EncryptedString should expire
        /// </summary>
        public DateTime ExpireTime
        {
            get
            {
                return _expireTime;
            }
            set
            {
                _expireTime = value;
                TimeSpan ts = _expireTime.Subtract(DateTime.Now);

                if (ts.Minutes < 1)
                    throw new InvalidOperationException("ExpireTime cannot be greater than the current time.");

                _expireMinutes = ts.Minutes;
            }
        }

        /// <summary>
        /// The number of hours in which the EncryptedString should expire
        /// </summary>
        public int ExpireMinutes
        {
            get
            {
                return _expireMinutes;
            }
            set
            {
                if (value < 1)
                    throw new InvalidOperationException("ExpireMinutes cannot be greater less than 1.");

                _expireMinutes = value;
                ExpireTime = DateTime.Now.AddMinutes(_expireMinutes);
            }
        }

        public string EncryptionKey
        {
            get { return _cryptoKey; }
            set { _cryptoKey = value; }
        }

        public const string MainKey = "data";

        #endregion
    }
}
